package com.example.demo.aop;

import com.example.demo.annotate.Repeat;
import com.example.demo.common.Func;
import com.example.demo.common.Valid;
import com.example.demo.common.constant.RedisKey;
import com.example.demo.common.constant.ResultCode;
import com.example.demo.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Order(10)
@RequiredArgsConstructor
@Slf4j
public class RepeatAop {

    private final RedisTemplate<String, String> redis;

    @Pointcut("@annotation(com.example.demo.annotate.Repeat)")
    public void RepeatAop() {

    }

    @Around("RepeatAop()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // redis的实体类没有被注入初始化，出错
        if (redis == null) {
            log.info(ResultCode.USER_REDIS_INIT_ERROR.getMessage());
            throw new BusinessException(ResultCode.USER_REDIS_INIT_ERROR);
        }

        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        Repeat requestRepeat = method.getAnnotation(Repeat.class);
        if (Valid.isEmpty(requestRepeat.prefix()))  {
            throw new BusinessException(ResultCode.REQUEST_ANNOTATE_ERROR);
        }

        StringBuilder sb = new StringBuilder();
        List<String> fieldList = Arrays.asList(requestRepeat.field());

        // 搜索接口的Request型参数的匹配字段
        final Object[] args = joinPoint.getArgs();
        final Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            // 如果参数字段在fieldList中，则拼接字段值
            if (fieldList.contains(parameters[i].getName())) {
                sb.append(requestRepeat.delimiter()).append(args[i]);
            }
        }

        // 搜索接口的DTO型参数的匹配字段
        if (Valid.isEmpty(sb.toString())) {
            // 获取方法上的多个注解（为什么是两层数组：因为第二层数组是只有一个元素的数组）
            final Annotation[][] parameterAnnotations = method.getParameterAnnotations();

            for (int i = 0; i < parameterAnnotations.length; i++) {
                // 获取DTO中所有的属性字段
                final Object object = args[i];
                final Field[] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                    // 如果DTO字段在fieldList中，则拼接字段值
                    if (fieldList.contains(field.getName())) {
                        field.setAccessible(true);
                        sb.append(requestRepeat.delimiter()).append(ReflectionUtils.getField(field, object));
                    }
                }
            }
        }

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String ip = Func.getIP(request);
        String key = String.format(RedisKey.REPEAT + "%s:%s", ip, requestRepeat.prefix() + sb);
        // key 还应加上用户ID，提交操作都是需要用户先登录，所以一定会有用户ID，可从redis中获取缓存的用户ID，并加入key中

        /*
        * 防重复提交的逻辑
        * 1、若key不存在redis中，则是正常访问，同时将key存入redis，防下次重复访问
        * 2、若key存在于redis中，则是重复访问，返回异常信息
        */
        String value = redis.opsForValue().get(key);
        if (Valid.isEmpty(value)) {
            redis.opsForValue().set(key, "1", requestRepeat.seconds(), TimeUnit.SECONDS);
        }
        else {
            log.info(ResultCode.REQUEST_REPEAT.getMessage());
            throw new BusinessException(ResultCode.REQUEST_REPEAT);
        }

        /*
         * 以下是加锁的逻辑（用了加锁逻辑，就不要用上述的“防重复提交逻辑”了）
         **/
        /*try {
            boolean lockAcquired = redis.opsForValue().setIfAbsent(key, Func.getULID());
            if (!lockAcquired) {
                log.info(ResultCode.REQUEST_REPEAT.getMessage());
                throw new BusinessException(ResultCode.REQUEST_REPEAT);
            }
            redis.expire(key, 30, TimeUnit.SECONDS);

            return joinPoint.proceed();
        }
        finally {
            redis.delete(key);
        }*/

        // 这个方法会随着缓存消失、锁失效，还是可以发起同样的请求，所以要真正做到接口幂等性，还需要业务代码的判断、设置数据库表的version校验等操作
        // 这个方法对于那些提交数据重复，但又真实需要的接口无法防抖，必须使用更复杂的传入提前生成的幂等校验id的方式

        return joinPoint.proceed();
    }
}
